﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace CPN.PJN.Silverlight.Extensions.Validation
{
    /// <summary>
    /// Classe de gestion de la validation pour INotifyDataErrorInfo & IDataErrorInfo
    /// </summary>
    public class ValidationManager
    {
        /// <summary>
        /// Nombre d'erreurs
        /// </summary>
        public int ErrorsCount
        {
            get { return _errors.Count; }
        }

        /// <summary>
        /// Vrai s'il y a des erreurs, faux sinon
        /// </summary>
        public bool HasErrors
        {
            get { return this._errors.Any(); }
        }

        /// <summary>
        /// Liste de messages d'erreur pour chaque propriété
        /// </summary>
        private readonly Dictionary<string, IEnumerable<string>> _errors = new Dictionary<string, IEnumerable<string>>();

        /// <summary>
        /// Liste de validateurs
        /// </summary>
        private readonly Dictionary<string, List<IValidationRule>> _validators = new Dictionary<string, List<IValidationRule>>();

        /// <summary>
        /// Action déclenchée lorsqu'une erreur a changé
        /// </summary>
        private readonly Action<string> _errorsChanged;

        /// <summary>
        /// Constructeur à utiliser en collaboration avec un IDataErrorInfo
        /// </summary>
        public ValidationManager()
        {
        }

        /// <summary>
        /// Constructeur à utiliser en collaboration avec un INotifyDataErrorInfo
        /// </summary>
        /// <param name="errorsChangedAction">Action déclenchée lorsque la liste d'erreurs est modifiée</param>
        public ValidationManager(Action<string> errorsChangedAction)
            : this()
        {
            _errorsChanged = errorsChangedAction;
        }

        #region Errors

        /// <summary>
        /// Efface les messages d'erreur d'une propriété
        /// </summary>
        /// <param name="propertyName">Nom de propriété</param>
        protected void ClearErrors<T>(Expression<Func<T>> property)
        {
            var propertyName = ExtractPropertyName(property);
            this.SetErrors(propertyName, new List<string>());
        }

        /// <summary>
        /// Efface les messages d'erreur d'une propriété
        /// </summary>
        /// <param name="propertyName">Nom de propriété</param>
        protected void ClearErrors(string propertyName)
        {
            this.SetErrors(propertyName, new List<string>());
        }

        /// <summary>
        /// Ajoute une erreur
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="property">Propriété</param>
        /// <param name="errorMessage">Message d'erreur</param>
        protected void SetErrors<T>(Expression<Func<T>> property, string errorMessage)
        {
            var propertyName = ExtractPropertyName(property);
            SetErrors(propertyName, new List<string> { errorMessage });
        }

        /// <summary>
        /// Initialise la liste des messages d'erreur d'une propriété
        /// </summary>
        /// <param name="propertyName">Nom de propriété</param>
        /// <param name="propertyErrors">Liste de messages d'erreur</param>
        protected void SetErrors(string propertyName, IEnumerable<string> propertyErrors)
        {
            if (propertyErrors.Any(error => error == null))
            {
                throw new ArgumentException("Errors messages should not be null", "propertyErrors");
            }

            var propertyNameKey = propertyName ?? string.Empty;

            IEnumerable<string> currentPropertyErrors;
            if (this._errors.TryGetValue(propertyNameKey, out currentPropertyErrors))
            {
                if (!AreErrorCollectionsEqual(currentPropertyErrors, propertyErrors))
                {
                    if (propertyErrors.Any())
                    {
                        _errors[propertyNameKey] = propertyErrors;
                    }
                    else
                    {
                        _errors.Remove(propertyNameKey);
                    }

                    if (_errorsChanged != null)
                        _errorsChanged(propertyNameKey);
                }
            }
            else
            {
                if (propertyErrors.Any())
                {
                    _errors[propertyNameKey] = propertyErrors;

                    if (_errorsChanged != null)
                        _errorsChanged(propertyNameKey);
                }
            }
        }

        /// <summary>
        /// Extrait un nom de propriété à partir d'une experssion
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="propertyExpresssion">Expression</param>
        /// <returns>Nom de propriété</returns>
        private string ExtractPropertyName<T>(Expression<Func<T>> propertyExpresssion)
        {
            if (propertyExpresssion == null)
            {
                throw new ArgumentNullException("propertyExpression");
            }

            var memberExpression = propertyExpresssion.Body as MemberExpression;
            if (memberExpression == null)
            {
                throw new ArgumentException("The expression is not a member access expression.", "propertyExpression");
            }

            var property = memberExpression.Member as PropertyInfo;
            if (property == null)
            {
                throw new ArgumentException("The member access expression does not access a property.", "propertyExpression");
            }

            var getMethod = property.GetGetMethod(true);
            if (getMethod == null)
            {
                // this shouldn't happen - the expression would reject the property before reaching this far
                throw new ArgumentException("The referenced property does not have a get method.", "propertyExpression");
            }

            if (getMethod.IsStatic)
            {
                throw new ArgumentException("The referenced property is a static property.", "propertyExpression");
            }

            return memberExpression.Member.Name;
        }

        /// <summary>
        /// Teste si 2 collections sont identiques
        /// </summary>
        /// <param name="propertyErrors">1ère collection</param>
        /// <param name="currentPropertyErrors">2ème collection</param>
        /// <returns>Vrai si les 2 collections sont identiques, faux sinon</returns>
        private static bool AreErrorCollectionsEqual(IEnumerable<string> propertyErrors, IEnumerable<string> currentPropertyErrors)
        {
            var equals = currentPropertyErrors.Zip(propertyErrors, (current, newError) => current == newError);
            return propertyErrors.Count() == currentPropertyErrors.Count() && equals.All(b => b);
        }

        #endregion

        /// <summary>
        /// Récupère la liste des erreurs d'une propriété
        /// <remarks>Si le nom de la propriété est null, retourne la liste de tous les messages d'erreur</remarks>
        /// </summary>
        /// <param name="propertyName">Nom de propriété</param>
        /// <returns></returns>
        public IEnumerable GetErrors(string propertyName)
        {
            if (string.IsNullOrEmpty(propertyName))
                return _errors.Values;

            IEnumerable<string> error;
            if (this._errors.TryGetValue(propertyName ?? string.Empty, out error))
                return error;

            return null;
        }

        /// <summary>
        /// Récupère le 1er message d'erreur d'une propriété
        /// </summary>
        /// <param name="propertyName">Nom de propriété (non null)</param>
        /// <returns></returns>
        public string GetError(string propertyName)
        {
            if (string.IsNullOrEmpty(propertyName))
            {
                throw new ArgumentNullException("propertyName");
            }

            IEnumerable<string> error;
            if (this._errors.TryGetValue(propertyName ?? string.Empty, out error))
                return error.FirstOrDefault();

            return null;
        }

        /// <summary>
        /// Efface toutes les règles de validation d'une liste de propriétés
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="propertiesToClear"></param>
        public void UnsetRuleFor<T>(params Expression<Func<T>>[] propertiesToClear)
        {
            foreach (var property in propertiesToClear)
            {
                var propertyName = ExtractPropertyName(property);
                if (this._validators.ContainsKey(propertyName))
                {
                    this._validators.Remove(propertyName);
                }

                ClearErrors(propertyName);
            }
        }

        /// <summary>
        /// Enregistre une règle de validation pour une propriété
        /// </summary>
        /// <typeparam name="T">Type de la propriété</typeparam>
        /// <param name="property">Propriété</param>
        /// <param name="validator">Règle de validation</param>
        public void SetRuleFor<T>(Expression<Func<T>> property, IValidator<T> validator)
        {
            SetRuleFor(property, validator, null);
        }

        /// <summary>
        /// Enregistre une règle de validation pour une propriété avec un message d'erreur explicite
        /// </summary>
        /// <typeparam name="T">Type de la propriété</typeparam>
        /// <param name="property">Propriété</param>
        /// <param name="validator">Règle de validation</param>
        /// <param name="message">Message d'erreur</param>
        public void SetRuleFor<T>(Expression<Func<T>> property, IValidator<T> validator, string message)
        {
            if (property == null)
            {
                throw new ArgumentNullException("property");
            }

            if (validator == null)
            {
                throw new ArgumentNullException("validator");
            }

            if (string.IsNullOrEmpty(validator.DefaultMessage) && string.IsNullOrEmpty(message))
            {
                throw new ArgumentException("The error message should not be null");
            }

            if (string.IsNullOrEmpty(message))
            {
                message = validator.DefaultMessage;
            }

            var validationRule = new ValidationRule<T>(property, validator, message);

            var propertyName = ExtractPropertyName(property);
            if (!this._validators.ContainsKey(propertyName))
            {
                this._validators[propertyName] = new List<IValidationRule>();
            }

            this._validators[propertyName].Add(validationRule);
        }

        /// <summary>
        /// Valide une propriété sans passer par les règles
        /// </summary>
        /// <typeparam name="T">Type</typeparam>
        /// <param name="property">Propriété</param>
        /// <param name="isValid">Vrai si la propriété est valide, faux sinon</param>
        /// <param name="errorMessage">Message d'erreur</param>
        public void Validate<T>(Expression<Func<T>> property, bool isValid, string errorMessage)
        {
            if (isValid)
            {
                ClearErrors(property);
            }
            else
            {
                SetErrors(property, errorMessage);
            }
        }

        /// <summary>
        /// Valide une propriété à partir de sa liste de règles de validation
        /// </summary>
        /// <param name="property">Propriété à valider</param>
        public void ValidateRules<T>(Expression<Func<T>> property)
        {
            var propertyName = ExtractPropertyName(property);
            ValidateRules(propertyName);
        }

        /// <summary>
        /// Valide une propriété à partir de sa liste de règles de validation
        /// </summary>
        /// <param name="propertyName">Propriété à valider</param>
        public void ValidateRules(string propertyName)
        {
            if (!_validators.ContainsKey(propertyName))
            {
                return;
            }

            IList<string> results = new List<string>();
            foreach (var validator in _validators[propertyName])
            {
                if (!validator.Validate())
                {
                    results.Add(validator.ErrorMessage);
                }
            }

            SetErrors(propertyName, results);
        }

        /// <summary>
        /// Valide toutes les propriétés enregistrées
        /// </summary>
        public void ValidateAllRules()
        {
            foreach (var pair in _validators)
            {
                ValidateRules(pair.Key);
            }
        }
    }
}